#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'middleman'
require 'nokogiri'
require 'rainbow/ext/string'
require 'uri'
require 'net/http'
require 'redcarpet'
require File.join(File.dirname(__FILE__), 'lib', 'custom_renderer')


task :test do

HTML = <<EOT
<div class="tabs">
  <div data-tab="PHP" data-lang="php">
```
Test 0 <>
Test 1 >
Test 3 <
Test 4 ><
Test 5 =>
Test 6 <=
Test 7 <>
<p><b>Test 8</b></p>
```
  </div>
</div>
EOT

HTML2 = <<EOT
<div class="tabs">
  <div data-tab="Ruby" data-lang="ruby">
```ruby
Test 0 <>
Test 1 >
Test 3 <
Test 4 ><
Test 5 =>
Test 6 <=
Test 7 <>
<p><b>Test</b></p>

# This is a ruby file.
class MyClass
  def foo
    'bar'
  end
end
```
  </div>
  <div data-tab="Plain">
This is a test of **markdown** inside a tab!

```
// This tab does not have the data-lang attribute set!
$ cd path/to/your/file
```
  </div>
  <div data-tab="HTML" data-lang="html">
```html
<p>Yes you can still use HTML in code blocks!</p>
```
  </div>
</div>
EOT

  def block_html(raw_html)
    done = raw_html.gsub(/(```.*?```)/m) do |match|
      markdown = Redcarpet::Markdown.new(CustomRenderer, fenced_code_blocks: true)
      markdown.render(match)
    end

    doc = Nokogiri::HTML::DocumentFragment.parse(done)
    nodes = doc.css('div.tabs > div')

    if nodes.empty?
      raw_html
    else
      ul = Nokogiri::XML::Node.new('ul', doc)
      ul['class'] = 'control'

      nodes.each do |node|
        title = node.attribute('data-tab').to_s
        lang = node.attribute('data-lang').to_s

        uuid = SecureRandom.uuid
        id = "tab-#{uuid}"

        li = Nokogiri::XML::Node.new('li', doc)
        li['data-lang'] = lang
        li.inner_html = %Q(<a href="##{id}">#{title}</a>)

        ul.add_child(li)

        node['id'] = id
      end

      nodes.first.before(ul)

      doc.to_html
    end
  end


  puts 'start block'
  puts block_html(HTML2)
  puts 'end block'
end


desc 'Check site for broken links'
task :check do

  sets = []
  cache = Sanity::Cache.new

  Dir["build/**/*.html"].each do |filename|
    p = Sanity::Page.new(filename, cache)

    sets << p.check_links
  end

  html = Sanity::Report::HTML.new(sets.map{ |s| s.to_html })
  File.open('sanity.html', 'w') { |file| file.write(html) }
end


module Sanity
  module Report

    class HTML
      include Padrino::Helpers

      HEADER = <<EOT
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Sanity Report</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">

  </head>
  <body>
    <div class="jumbotron">
      <div class="container-fluid">
        <h1>Sanity Report</h1>
      </div>
    </div>
    <div class="container-fluid">

EOT
      FOOTER = <<EOT
    </div>
  </body>
</html>
EOT




      def initialize(content)
        if content.respond_to?(:to_html)
          html = content.to_html
        else
          html = content
        end

        @content = content_tag(:div, html, id: 'content')
      end

      def to_html
        "#{HEADER}#{@content}#{FOOTER}"
      end

      def to_s
        to_html
      end
    end
  end

  class ResultSet
    include Padrino::Helpers
    attr_accessor :set
    def initialize(path, set = [])
      @path = path
      @set = set
    end

    def push(item)
      @set.push(item)
    end

    def to_html
      content_tag(:h2, @path) <<
      content_tag(:table, class: 'table table-striped') do
        content_tag(:thead) do
          content_tag(:tr) do
            content_tag(:th, 'Type') <<
            content_tag(:th, 'Status') <<
            content_tag(:th, 'URI') <<
            content_tag(:th, 'Message') <<
            content_tag(:th, 'Path')
          end
        end <<
        content_tag(:tbody) do
          @set.map do |item|
            item.to_html
          end
        end
      end
    end
  end

  class Result
    include Padrino::Helpers
    attr_accessor :type, :status, :path, :uri, :message, :cache, :backtrace

    def initialize
    end

    def exception=(e)
      @status = :exception
      @message = e.message
      @backtrace = e.backtrace
    end

    def to_s
      "#{@type} [#{@status}] #{@uri} #{@message} #{@path}".color(terminal_color)
    end

    def to_html
      content_tag(:tr, class: bootstrap_css_class) do
        content_tag(:td, @type) <<
        content_tag(:td, @status) <<
        content_tag(:td, @uri) <<
        content_tag(:td, @message) <<
        content_tag(:td, @path)
      end
    end



    private

    def bootstrap_css_class
      case @status
      when :success
        'success'
      when :info
        'info'
      when :warning
        'warning'
      when :error
        'danger'
      when :exception
        'danger'
      else
        raise ArgumentError, "Status `#{@status}` is not a valid type"
      end
    end

    def terminal_color
      case @status
      when :success
        :green
      when :info
       :blue
      when :warning
        :yellow
      when :error
        :red
      when :exception
        :red
      else
        raise ArgumentError, "Status `#{@status}` is not a valid type"
      end
    end
  end

  class Cache
    def initialize(store = {})
      @store = store
    end

    def read(uri)
      @store[uri]
    end

    def write(uri, value)
      @store[uri] = value
    end

    def fetch(uri)
      if block_given?
        if exists?(uri)
          read(uri)
        else
          write(uri, yield)
        end
      else
        read(uri)
      end
    end

    def exists?(uri)
      @store.has_key?(uri)
    end
  end

  class Page
    INDEX_FILE = 'index.html'

    def initialize(filename, cache = Sanity::Cache.new)
      @filename = filename
      @cache = cache
      f = File.open(@filename)
      @doc = Nokogiri::HTML(f)

      @build_path = File.join(Middleman::Application.root, 'build')
      f.close
    end

    def check_links
      rs = Sanity::ResultSet.new(@filename)
      @doc.css('a').each do |link|
        uri = link['href']
        r = check_href(uri)
        r.path = @filename
        puts r
        rs.push(r)
      end
      rs
    end

    def check_href(href)
      # TODO: add trailing slash, relative url, and in page anchor links.
      # TODO: Test for missing titles!
      # TODO: Test for unneeded .html extension!
      # TODO: Switch from Nokogir to raw ID checker
      case href
      when /\A\s*\z/, nil
        check_empty_href(href)
      when /\A(https?):\/\/.+\z/
        check_external_href(href)
      when /\A#.+\z/
        check_anchor_href(href)
      when /\A#\z/
        check_empty_anchor_href(href)
      when /\A\/\z/
        check_root_href(href)
      when /\Amailto:.+\z/
        check_mailto_href(href)
      else
        check_internal_href(href)
      end
    end

    def check_external_href(href)
      r = Sanity::Result.new
      r.uri = href
      r.type = :external_uri
      begin
        response = @cache.fetch(href) do
          r.cache = :miss
          uri = URI(href)
          Net::HTTP.get_response(uri)
        end

        case response
        when Net::HTTPSuccess
          r.status = :success
        when Net::HTTPNotFound
          r.status = :error
        when Net::HTTPRedirection
          location = response['location']
          r.status = :info
          r.message = "Redirect: #{location}"
        else
          r.status = :warning
          r.message = "Response: #{response.class}"
        end
      rescue => e
        r.exception = e
      end
      r
    end

    def check_anchor_href(href)
      r = Sanity::Result.new
      r.uri = href
      r.type = :anchor
      begin
        result = @doc.css(href)

        if result.count > 0
          r.status = :success
        else
          r.status = :error
        end

      rescue => e
        r.exception = e
      end
      r
    end

    def check_empty_anchor_href(href)

      r = Sanity::Result.new
      r.uri = href
      r.type = :empty_anchor
      r.status = :info
      r
    end

    def check_root_href(href)
      r = Sanity::Result.new
      r.uri = href
      r.type = :root_path

      filename = File.join(@build_path, INDEX_FILE)
      if File.exist?(filename)
        r.status = :success
      else
        r.status = :error
        r.message "Not found: #{filename}"
      end

      r
    end

    def check_mailto_href(href)
      r = Sanity::Result.new
      r.uri = href
      r.type = :mailto

      uri = URI.parse(href)
      if uri.is_a?(URI::MailTo)
        r.status = :success

      else
        r.status = :error
      end

      r
    end

    def check_empty_href(href)
      r = Sanity::Result.new
      r.uri = href
      r.type = :empty_uri
      r.status = :error
      r
    end

    def check_internal_href(href)

      r = Sanity::Result.new
      r.uri = href
      r.type = :internal_uri

      filename = File.join(@build_path, href.gsub('/', File::SEPARATOR))
      if File.directory?(filename)
        filename = File.join(filename, INDEX_FILE)
      end

      if File.exist?(filename)

        r.status = :success
      else

        r.status = :error
      end

      r
    end
  end
end


